﻿using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace DimensionsHelper.SourceGeneration.CommandLine
{

    public sealed partial class ArgumentSourceGenerator
    {

        private static class ArgumentSourceEmitter
        {
            private const string ArgumentPropertyInfoRef = "global::DimensionsHelper.Serialization.CommandLine.ArgumentPropertyInfo";
            private const string ArgumentDictionaryInfoRef = "global::DimensionsHelper.Serialization.CommandLine.ArgumentDictionaryInfo";
            private const string ArgumentObjectInfoRef = "global::DimensionsHelper.Serialization.CommandLine.ArgumentObject";
            private const string ArgumentKindRef = "global::DimensionsHelper.Serialization.CommandLine.ArgumentKind";

            private const string DefaultListSeparator = ";";


            public static void Emit(GeneratorExecutionContext context, GenerationInfo info)
            {
                Emit(info, context.AddSource);
            }


            public static void Emit(SourceProductionContext context, GenerationInfo info)
            {
                Emit(info, context.AddSource);
            }


            public static void Emit(GenerationInfo info, Action<string, string> addSourceHandler)
            {
                var initializers = new List<string>();
                foreach (var obj in info.Objects)
                {
                    addSourceHandler($"{info.ContextClassName}.{obj.TypeName}.g.cs", CreateObjectSourceWriter(obj, initializers).ToString());
                }

                // 添加初始化类
                var writer = new SourceWriter();
                writer.AppendLine($@"
// <auto-generated/>

#nullable enable

namespace {info.Namespace}");
                writer.AppendLine('{');
                writer.IndentLevel++;

                // class
                writer.AppendLine();
                writer.AppendLine($"{(info.IsInternal ? "internal" : "public")} partial class {info.ContextClassName}");
                writer.AppendLine('{');
                writer.IndentLevel++;

                // initialize
                writer.AppendLine();
                writer.AppendLine("// Initialize");
                writer.AppendLine($"protected override void Initialize()");
                writer.AppendLine('{');
                writer.IndentLevel++;

                foreach (var initializer in initializers)
                {
                    writer.AppendLine($"{initializer}();");
                }

                writer.IndentLevel--;
                writer.AppendLine('}');

                // 单例
                writer.AppendLine();
                writer.AppendLine($@"
/// <summary>
/// Singleton
/// </summary>
public static {info.ContextClassName} Default {{ get; }} = new {info.ContextClassName}();
                ");

                writer.IndentLevel--;
                writer.AppendLine();
                writer.AppendLine('}');

                writer.IndentLevel--;
                writer.AppendLine();
                writer.AppendLine('}');

                addSourceHandler($"{info.ContextClassName}.g.cs", writer.ToString());
            }


            private static SourceWriter CreateObjectSourceWriter(ObjectGenerationInfo generation, List<string> initializers)
            {
                var writer = new SourceWriter();
                writer.AppendLine($@"
// <auto-generated/>

#nullable enable

namespace {generation.Namespace}");
                writer.AppendLine('{');
                writer.AppendLine();
                writer.IndentLevel++;
                WriteArgumentContextObject(writer, generation, initializers);
                writer.IndentLevel--;
                writer.AppendLine();
                writer.AppendLine('}');
                return writer;
            }


            private static void WriteArgumentContextObjectInitializer(SourceWriter writer, ObjectGenerationInfo info, string initializerName, List<string> propertyObjectGetters)
            {
                writer.AppendLine($"private void {initializerName}()");
                writer.AppendLine('{');
                writer.IndentLevel++;

                string propertiesName = string.Empty;
                string dictPropertiesName = string.Empty;

                // properties array
                if (info.RegularProperties.Length > 0)
                {
                    propertiesName = "props";
                    writer.AppendLine($@"
// regular properties
var {propertiesName} = new {ArgumentPropertyInfoRef}[{info.RegularProperties.Length}];");
                    for (int i = 0; i < info.RegularProperties.Length; i++)
                    {
                        WriteArgumentContextObjectProperty(writer, info.RegularProperties[i], $"{propertiesName}[{i}]", propertyObjectGetters);
                    }
                }

                // dictionary array
                if (info.DictionaryProperties.Length > 0)
                {
                    dictPropertiesName = "dictProps";
                    writer.AppendLine($@"
// dictionary properties
var {dictPropertiesName} = new {ArgumentDictionaryInfoRef}[{info.DictionaryProperties.Length}];");
                    for (int i = 0; i < info.DictionaryProperties.Length; i++)
                    {
                        WriteArgumentContextObjectDictionaryProperty(writer, info.DictionaryProperties[i], $"{dictPropertiesName}[{i}]", propertyObjectGetters);
                    }
                }

                writer.AppendLine($@"
// ArgumentObject
var argObj = new {ArgumentObjectInfoRef}
{{
    Properties = {(string.IsNullOrEmpty(propertiesName) ? $"Array.Empty<{ArgumentPropertyInfoRef}> ()" : propertiesName)},
    Dictionaries = {(string.IsNullOrEmpty(dictPropertiesName) ? $"Array.Empty<{ArgumentDictionaryInfoRef}>()" : dictPropertiesName)},
    ObjectCreator = static () => new {info.TypeFullName}(),
}};

// register object
Register(typeof({info.TypeFullName}), argObj);");

                writer.IndentLevel--;
                writer.AppendLine('}');

                // property object getters
                if (propertyObjectGetters.Count > 0)
                {
                    foreach (var method in propertyObjectGetters)
                    {
                        writer.AppendLine();
                        writer.AppendLine(method);
                    }
                }
            }


            private static void WriteArgumentContextObject(SourceWriter writer, ObjectGenerationInfo info, List<string> initializers)
            {
                writer.AppendLine($"{(info.IsInternal ? "internal" : "public")} partial class {info.ContextClassName}");
                writer.AppendLine('{');
                writer.IndentLevel++;

                var propertyObjectGetters = new List<string>();
                string initMethodName = $"{info.TypeName}_Initialize";
                initializers?.Add(initMethodName);
                // 初始化函数
                WriteArgumentContextObjectInitializer(writer, info, initMethodName, propertyObjectGetters);
                writer.IndentLevel--;
                writer.AppendLine('}');
            }


            private static string BuildPropertyObjectGetter(ObjectPropertyGenerationInfo property, List<string> cache)
            {
                string methodName = $"{property.ClassName}_{property.PropertyName}_ObjectGetter";
                string methodBody;

                // 参数列表是空时，从上下文对象中查找
                if (property.ObjectProperties.IsEmpty && property.ObjectDictionaryProperties.IsEmpty)
                {
                    methodBody = $@"
private {ArgumentObjectInfoRef}? {methodName}()
{{
    return GetArgumentObject(typeof({property.OriginalTypeFullName}));
}}";
                }
                else
                {
                    var writer = new SourceWriter();
                    writer.AppendLine($@"
private {ArgumentObjectInfoRef}? {methodName}()");
                    writer.AppendLine('{');
                    writer.IndentLevel++;

                    string propertiesName = string.Empty;
                    string dictionariesName = string.Empty;

                    if (!property.ObjectProperties.IsEmpty)
                    {
                        propertiesName = "props";
                        writer.AppendLine($@"
var {propertiesName} = new {ArgumentPropertyInfoRef}[{property.ObjectProperties.Length}];");

                        for (int i = 0; i < property.ObjectProperties.Length; i++)
                        {
                            WriteArgumentContextObjectProperty(
                                writer,
                                property.ObjectProperties[i], 
                                $"{propertiesName}[{i}]", 
                                cache);
                        }
                    }

                    if (!property.ObjectDictionaryProperties.IsEmpty)
                    {
                        dictionariesName = "dicts";
                        writer.AppendLine($@"
var {dictionariesName} = new {ArgumentDictionaryInfoRef}[{property.ObjectDictionaryProperties.Length}];");

                        for (int i = 0; i < property.ObjectDictionaryProperties.Length; i++)
                        {
                            WriteArgumentContextObjectDictionaryProperty(
                                writer,
                                property.ObjectDictionaryProperties[i],
                                $"{dictionariesName}[{i}]",
                                cache);
                        }
                    }

                    writer.AppendLine($@"
// ArgumentObject
var argObj = new {ArgumentObjectInfoRef}
{{
    Properties = {(string.IsNullOrEmpty(propertiesName) ? $"Array.Empty<{ArgumentPropertyInfoRef}> ()" : propertiesName)},
    Dictionaries = {(string.IsNullOrEmpty(dictionariesName) ? $"Array.Empty<{ArgumentDictionaryInfoRef}>()" : dictionariesName)},
    ObjectCreator = static () => new {property.ListElementType ?? property.ImplTypeFullName}(),
}};

return argObj;");

                    writer.IndentLevel--;
                    writer.AppendLine('}');
                    methodBody = writer.ToString();
                }

                cache.Add(methodBody);
                return methodName;
            }


            private static void WriteArgumentContextObjectProperty(SourceWriter writer, ObjectPropertyGenerationInfo info, string leftName, List<string> propertyObjectGetters)
            {
                var subProperty = !string.IsNullOrEmpty(info.PropertyBinding) ? $".{info.PropertyBinding}" : "";
                var setter = $@"static (o, v) => 
    {{
        if (v is not null) 
        {{
            (({info.ClassFullName})o).{info.PropertyName}{subProperty} = ({info.ImplTypeFullName})v;
        }}
    }}";
                var getter = $"static o => (({info.ClassFullName})o).{info.PropertyName}{subProperty}";
                string listSep = info.ListSeparator ?? DefaultListSeparator;

                if (info.IsBasicType)
                {
                    writer.AppendLine($@"
// {info.ClassName}.{info.PropertyName}
{leftName} = new {ArgumentPropertyInfoRef}({ArgumentKindRef}.Regular, ""{info.ArgumentName ?? ""}"", typeof({info.OriginalTypeFullName})){(info.IsDictionaryKey || info.IsDictionaryValue ? "" : $@"
{{
    CanFollow = {(info.CanFollow ? "true" : "false")},
    ListSeparator = ""{listSep}"",
    Getter = {(info.IsWriteOnly ? "null" : getter)},
    Setter = {(info.IsReadOnly ? "null" : setter)},
}}")};");
                }
                else if (info.IsList)
                {
                    string listElementObject = "null";
                    // 处理多个注解绑定属性值类型的属性的情况
                    if (!info.ObjectProperties.IsEmpty || !info.ObjectDictionaryProperties.IsEmpty)
                    {
                        listElementObject = $"{BuildPropertyObjectGetter(info, propertyObjectGetters)}()";
                    }

                    writer.AppendLine($@"
// {info.ClassName}.{info.PropertyName}
{leftName} = new {ArgumentPropertyInfoRef}({ArgumentKindRef}.List, ""{info.ArgumentName ?? ""}"", typeof({info.OriginalTypeFullName}))
{{
    CanFollow = {(info.CanFollow ? "true" : "false")},
    ListSeparator = ""{listSep}"",
    Getter = {(info.IsWriteOnly ? "null" : getter)},
    Setter = {(info.IsReadOnly ? "null" : setter)},
    ObjectCreator = static () => new {info.ImplTypeFullName}(),
    ListAddHandler = static (o, v) => 
    {{
        if (v is not null)
        {{
            (({info.OriginalTypeFullName})o).Add(({info.ListElementType})v);
        }}
    }},
    ListElementObject = {listElementObject}
}};");
                }
                // object
                else
                {
                    Debug.Assert(info.IsObjectRef);
                    writer.AppendLine($@"
// {info.ClassName}.{info.PropertyName}
{leftName} = new {ArgumentPropertyInfoRef}({ArgumentKindRef}.Object, """", typeof({info.OriginalTypeFullName}))
{{
    Getter = {(info.IsWriteOnly ? "null" : getter)},
    Setter = {(info.IsReadOnly ? "null" : setter)},
    ObjectCreator = static () => new {info.ImplTypeFullName}(),
    PropertyObjectGetter = {BuildPropertyObjectGetter(info, propertyObjectGetters)},
}};");
                }
            }


            private static void WriteArgumentContextObjectDictionaryProperty(SourceWriter writer, ObjectDictionaryPropertyGenerationInfo info, string leftName, List<string> propertyObjectGetters)
            {
                // Key
                string keyName = $"{info.PropertyName}_Key";
                // Value
                string valName = $"{info.PropertyName}_Val";
                writer.AppendLine($"{ArgumentPropertyInfoRef} {keyName}, {valName};");

                WriteArgumentContextObjectProperty(writer, info.Key, keyName, propertyObjectGetters);
                WriteArgumentContextObjectProperty(writer, info.Value, valName, propertyObjectGetters);
                // typeName
                string originalName = info.DictionaryType;
                string normalizedName = info.DictionaryValueType;

                string getter = $"static () => new {normalizedName}()";
                string setter = $@"static (o, v) => 
    {{
        if (v is not null) 
        {{
            (({info.ClassFullName})o).{info.PropertyName} = ({info.DictionaryType})v;
        }}
    }}";

                writer.AppendLine($@"
// {info.ClassName}.{info.PropertyName}
{leftName} = new {ArgumentDictionaryInfoRef}(typeof({originalName}), {keyName}, {valName})
{{
    ObjectCreator = {(info.IsWriteOnly ? "null" : getter)},
    Setter = {(info.IsReadOnly ? "null" : setter)},
    Getter = static o => (({info.ClassFullName})o).{info.PropertyName},
    DictionaryAddHandler = static (o, k, v) =>
    {{
        if (k is not null && v is not null)
        {{
            (({originalName})o).TryAdd(({info.Key.OriginalTypeFullName})k, ({info.Value.OriginalTypeFullName})v);
        }}
    }},
}};");
            }

        }

    }
}